<?php
/**
 * Fastapp.
 * 3/7/24
 * @Author deepsea
 * @Contact telegram:deepsea159
 */

namespace App\Fastapp\Api\Service\Account;

use App\Fastapp\Api\Service\BaseService;
use App\Fastapp\Api\Service\Telegram;
use App\Fastapp\Exception\TipException;
use App\Fastapp\FastappEmailService;
use App\Fastapp\FastappSmsService;
use App\Fastapp\Listener\Event\AccountBindEmailEvent;
use App\Fastapp\Listener\Event\AccountBindMobileEvent;
use App\Fastapp\Listener\Event\AccountLevelEvent;
use App\Fastapp\Listener\Event\AccountLoginEvent;
use App\Fastapp\Listener\Event\AccountRegisterEvent;
use App\Fastapp\Model\Account;
use App\Fastapp\Model\AccountInfo;
use App\Fastapp\Model\AccountInviteLevel;
use App\Fastapp\Model\AccountOtherAddress;
use App\Fastapp\Model\AccountOtherRelated;
use App\Fastapp\Model\AccountPushClient;
use App\Fastapp\Model\AccountRealname;
use App\Fastapp\Model\AccountSecurityKey;
use App\Fastapp\Model\AccountSetting;
use App\Fastapp\Model\FastappChannelAgent;
use EasyWeChat\MiniApp\Application;
use Hyperf\DbConnection\Db;
use Mine\Helper\Str;

class AccountService extends BaseService
{
    public function loginByPassword(array $data): array
    {
        $data['type'] = fastapp_header('account-type', 1);
        $md = Account::query()
            ->where(['type' => $data['type']])
            ->whereRaw('(username=? or mobile=? or email=?)', [$data['username'], $data['username'], $data['username']])->first();
        if (!$md) throw new TipException(fastapp_t('account.account_not_registered'));
        if ($md->status == 2) throw new TipException(fastapp_t('account.account_cancelled'));
        if (!$md::passwordVerify($data['password'], (string)$md->password)) throw new TipException(fastapp_t('account.password_error'));
        if (!empty($data['push_client'])) $this->setPushClient($data['push_client'], $md->gene_id);
        return $this->tokenFormat($md);
    }

    public function loginByVcode(array $data): array
    {
        FastappSmsService::validate($data['mobile'], $data['vcode'], $data['code']);
        $data['type'] = fastapp_header('account-type', 1);
        $md = Account::query()->where(['mobile' => $data['mobile'], 'type' => $data['type']])->first();
        if ($md) {
            if ($md->status == 2) throw new TipException(fastapp_t('account.account_cancelled'));
            if (!empty($data['push_client'])) $this->setPushClient($data['push_client'], $md->gene_id);
            return $this->tokenFormat($md);
        }
        if (empty($data['nickname'])) $data['nickname'] = substr($data['mobile'], 0, 3) . '****' . substr($data['mobile'], -4);
        return $this->tokenFormat($this->create($data));
    }

    public function registerByVcode(array $data): array
    {
        $data['type'] = fastapp_header('account-type', 1);
        $map['mobile'] = $data['mobile'];
        $map['code'] = $data['code'];
        $map['type'] = $data['type'];
        if (!empty($data['email'])) $map['email'] = $data['email'];
        FastappSmsService::validate($data['mobile'], $data['vcode'], $data['code']);
        $md = Account::query()->where($map)->first();
        if (!$md) {
            if (empty($data['nickname'])) $data['nickname'] = substr($data['mobile'], 0, 3) . '****' . substr($data['mobile'], -4);
            $md = $this->create($data);
        }
        api_dispatch(new AccountBindMobileEvent($md->gene_id, $md->code, $md->mobile));
        return $this->tokenFormat($md);
    }

    public function registerByUsername(array $data): array
    {
        $data['type'] = fastapp_header('account-type', 1);
        $map['type'] = $data['type'];
        $map['username'] = $data['username'];
        $md = Account::query()->where($map)->first();
        if (!$md) {
            if (empty($data['nickname'])) $data['nickname'] = substr_replace($data['username'], '***', (int)(strlen($data['username']) / 2 - 1), 4);
            $md = $this->create($data);
            return $this->tokenFormat($md);
        }
        throw new TipException(fastapp_t('account.account_registered'));
    }

    public function registerByEmail(array $data): array
    {
        $data['type'] = fastapp_header('account-type', 1);
        $map['type'] = $data['type'];
        $map['email'] = $data['email'];
        $md = Account::query()->where($map)->first();
        if ($md) throw new TipException(fastapp_t('account.account_registered'));
        FastappEmailService::validate($data['email'], $data['vcode']);
        if (empty($data['nickname'])) $data['nickname'] = substr_replace($data['mobile'], '***', (int)(strlen($data['mobile']) / 2 - 1), 4);
        $md = $this->create($data);
        api_dispatch(new AccountBindEmailEvent($md->gene_id, $md->email));
        return $this->tokenFormat($md);
    }

    public function create(array $data): Account
    {
        $data = array_filter($data);
        if (empty($data['channel'])) $data['channel'] = redis()->hGet('channel', fastapp_header('channel')) ?: 1;
        Db::beginTransaction();
        try {
            $data['gene_id'] = redis()->hIncrBy('gene', 'uid', 1);
            $data['no'] = redis()->hIncrBy('gene', 'uno', 1);
            if (empty($data['mobile'])) $data['code'] = '';
            if (isset($data['id'])) unset($data['id']);
            $md = new Account();
            $md->fill(array_merge($data, ['invite_code' => Str::random(7) . ($data['gene_id'] % 9)]));
            $md->save();
            $data['user_id'] = $md->gene_id;
            $info = new AccountInfo();
            $info->fill($data);
            $info->save();
            if (!empty($data['push_client'])) $this->setPushClient($data['push_client'], $md->gene_id);
            if (!empty($data['invite_code'])) $this->bindLevel($md->gene_id, $data['invite_code'], $md->type);
            Db::commit();
            set_user_cache(['channel' => $md->channel], $md->gene_id);
            $symbol = api_cache()->getAppSetting($md->type)['symbol'] ?? '';
            if ($symbol) $symbol = implode(',', [$symbol, 'USDT']); else $symbol = 'USDT';
            api_dispatch(new AccountRegisterEvent($md->gene_id, $md->type, $symbol));
            return $md;
        } catch (\Throwable $throwable) {
            sys_notify('register:' . $throwable->getMessage());
            Db::rollBack();
        }
        throw new TipException(fastapp_t('account.registration_failure'));
    }

    public function setPushClient(string $push_client, int $user_id): bool
    {
        $md = AccountPushClient::query()->firstOrCreate(['user_id' => $user_id, 'type' => 1]);
        return AccountPushClient::query()->where(['id' => $md->id])->update(['client_id' => $push_client]);
    }

    public function bindLevel(int $user_id, string $invite_code, int $account_type): void
    {
        $parent_id = Account::query()->where(['invite_code' => $invite_code])->value('gene_id');
        if (!$parent_id) return;
        $md = AccountInviteLevel::query()->where(['user_id' => $parent_id])->first();
        if ($md) {
            $link = $md->link . (str_ends_with($md->link, '|') ? '' : ',') . $parent_id;
        } else {
            $link = $parent_id . '|';
        }
        AccountInviteLevel::query()->create([
            'user_id' => $user_id,
            'parent_id' => $parent_id,
            'link' => $link,
        ]);
        api_dispatch(new AccountLevelEvent($user_id, $parent_id, $account_type));
    }

    public function checkAccount(array $data): array
    {
        $data['type'] = fastapp_header('account-type', 1);
        return ['is_register' => (int)Account::query()->where($data)->exists()];
    }

    public function tokenFormat(Account $md): array
    {
        $info = self::info($md);
        $scene = fastapp_header('app-source');
        $info['token'] = user($scene)->getToken(['id' => $md->gene_id]);
        list($ip, $region, $agent) = getClientInfo();
        api_dispatch(new AccountLoginEvent($md->gene_id, $md->type, $ip, $region, $agent));
        $symbol = explode(',', api_cache()->getAppSetting($md->type)['symbol'] ?? 'USDT')[0];
        $currency_id = redis()->hGet('currency', $symbol);
        $ucache = [
            'mobile' => $md->mobile,
            'code' => $md->code,
            'type' => $md->type,
            'symbol' => $symbol,
            'currency_id' => $currency_id,
            'email' => $md->email,
            'channel' => $md->channel,
            'invite_code' => $md->invite_code,
            'lang' => fastapp_lang(),
        ];
        set_user_cache($ucache, $md->gene_id);
        return $info;
    }

    public function changePassword(array $data): bool
    {
        $ac = Account::query()->where(['gene_id' => get_uid()])->firstOrFail();
        if (!$ac->passwordVerify($data['original_password'], $ac->password)) throw new TipException(fastapp_t('account.original_mobile_error'));
        if ($ac->update(['password' => $data['password']])) return true;
        throw new TipException(fastapp_t('fastapp.modification_failed'));
    }

    public function unsubscribe(array $data): bool
    {
        $ac = Account::query()->where(['gene_id' => get_uid()])->firstOrFail();
        if ($ac->mobile != $data['mobile']) throw new TipException(fastapp_t('account.original_mobile_error'));
        FastappSmsService::validate($data['mobile'], $data['vcode'], $data['code']);
        if ($ac->update(['status' => 3])) return true;
        throw new TipException(fastapp_t('account.cancellation_failed'));
    }

    public function info(?Account $md = null): array
    {
        if (!$md) $md = Account::query()->select(['gene_id', 'no', 'mobile', 'username', 'email', 'invite_code'])->where(['gene_id' => get_uid()])->first();
        $info = AccountInfo::query()->where(['user_id' => $md->gene_id])->first()->toArray();
        $info['no'] = $md->no;
        $info['code'] = $md->code;
        $info['mobile'] = $md->mobile ? substr($md->mobile, 0, 3) . '****' . substr($md->mobile, -4) : '';
        $info['username'] = $md->username ?? '';
        $info['email'] = $md->email ?? '';
        $info['invite_code'] = $md->invite_code;
        $info['is_wechat'] = AccountOtherRelated::query()->where(['user_id' => $md->gene_id])->whereIn('related_type', [1, 2])->exists() ? 1 : 0;
        $info['is_realname'] = AccountRealname::query()->where(['user_id' => $md->gene_id, 'type' => 1])->exists() ? 1 : 0;
        $info['is_agent'] = FastappChannelAgent::query()->where(['user_id' => $md->gene_id])->exists() ? 1 : 0;
        $ks = AccountSecurityKey::query()->where(['user_id' => $md->gene_id])->pluck('status', 'type')->toArray();
        $config = AccountSettingService::config($md->gene_id);
        $info['is_google2FA'] = empty($ks[1]) ? 0 : 1;
        $info['is_email'] = empty($ks[2]) ? 0 : 1;
        $info['is_mobile'] = empty($ks[3]) ? 0 : 1;
        $info['is_transaction_password'] = empty($config['transaction_password']) ? 0 : 1;
        return $info;
    }

    public function updateInfo(array $data): array
    {
        $md = AccountInfo::query()->with(['account'])->where(['user_id' => get_uid()])->first();
        $md->fill($data);
        $md->save();
        return $this->info($md->account);
    }

    public function loginByWx(array $data): array
    {
        if ($data['related_type'] == 2) $open_id = $this->wxMini($data['js_code']);
        elseif ($data['related_type'] == 3) $open_id = $this->wxApp($data['js_code']);
        $data['type'] = fastapp_header('account-type', 1);
        if (!Account::query()->where(['code' => $data['code'], 'mobile' => $data['mobile'], 'type' => $data['type']])->exists()) throw new TipException(fastapp_t('account.mobile_not_registered'));
        $map = [
            'unique_code' => $open_id,
            'related_type' => $data['related_type'],
            'account_type' => $data['type']
        ];
        $md = AccountOtherRelated::query()->firstOrCreate($map);
        if ($md && $md->account) {
            if ($md->account->status == 2) throw new TipException(fastapp_t('account.account_cancelled'));
            if (!empty($data['push_client'])) $this->setPushClient($data['push_client'], $md->account->gene_id);
            return $this->tokenFormat($md->account);
        }
        if (!empty($data['direct'])) {
            $md = $this->create($data);
            return $this->tokenFormat($md);
        }
        return [
            'to_bind' => 1,
            'unique_code' => $open_id
        ];
    }

    public function bindWx(array $data): array
    {
        FastappSmsService::validate($data['mobile'], $data['vcode'], $data['code']);
        $data['type'] = fastapp_header('account-type', 1);
        $md = Account::query()->where(['code' => $data['code'], 'mobile' => $data['mobile'], 'type' => $data['type']])->first();
        if ($data['related_type'] == AccountOtherRelated::TYPE_WECHAT_OFFICIAL && !$md) {
            $md = $this->create($data);
            $avatar = base642image(file_get_contents($data['avatar']), '/avatar/' . date('Ymd'), is_binary: true, uid: $md->gene_id);
            AccountInfo::query()->where(['user_id' => $md->gene_id])->update(['avatar' => $avatar]);
        }
        $map = [
            'unique_code' => $data['unique_code'],
            'related_type' => $data['related_type'],
            'account_type' => $data['type']
        ];
        AccountOtherRelated::query()->where($map)->update(['user_id' => $md->gene_id]);
        return $this->tokenFormat($md);
    }

    public function wxMini(string $js_code): string
    {
        $c = api_cache()->getConfigByGroupKey('wechat');
        $config = [
            'app_id' => $c['mini_app_id'],
            'secret' => $c['mini_secret'],
            'token' => $c['mini_token'],
            'aes_key' => $c['mini_aes_key'],
            'http' => [
                'throw' => true, // 状态码非 200、300 时是否抛出异常，默认为开启
                'timeout' => 5.0,
                // 'base_uri' => 'https://api.weixin.qq.com/', // 如果你在国外想要覆盖默认的 url 的时候才使用，根据不同的模块配置不同的 uri
                'retry' => true, // 使用默认重试配置
            ],
        ];
        $app = new Application($config);
        $res = $app->getClient()->get('/sns/jscode2session', [
            'query' => [
                'grant_type' => 'authorization_code',
                'js_code' => $js_code,
                'appid' => $c['mini_app_id'],
                'secret' => $c['mini_secret'],
            ]
        ]);
        $res = json_decode($res->getContent(), true);
        return $res['openid'];
    }

    public function wxApp(string $js_code): string
    {
        $c = api_cache()->getConfigByGroupKey('wechat');
        $config = [
            'app_id' => $c['open_app_id'],
            'secret' => $c['open_app_secret'],
            'http' => [
                'throw' => true, // 状态码非 200、300 时是否抛出异常，默认为开启
                'timeout' => 5.0,
                // 'base_uri' => 'https://api.weixin.qq.com/', // 如果你在国外想要覆盖默认的 url 的时候才使用，根据不同的模块配置不同的 uri
                'retry' => true, // 使用默认重试配置
            ],
        ];
        $app = new Application($config);
        $res = $app->getClient()->get('/sns/oauth2/access_token', [
            'query' => [
                'grant_type' => 'authorization_code',
                'code' => $js_code,
                'appid' => $c['open_app_id'],
                'secret' => $c['open_app_secret'],
            ]
        ]);
        $res = json_decode($res->getContent(), true);
        return $res['openid'];
    }

    public function setCache(array $data): bool
    {
        $uid = get_uid();
        //symbol
        if ($data['field'] == 'currency') {
            $currency_id = redis()->hGet('currency', $data['val']);
            return set_user_cache(['symbol' => $data['val'], 'currency_id' => $currency_id], $uid);
        }
        return set_user_cache([$data['field'] => $data['val']], $uid);
    }

    public function sendVcode(array $data): bool
    {
        if ($data['type'] == 1) return FastappSmsService::sendCode($data['mobile'], $data['code'], fastapp_header('account-type', 1));
        else  return FastappEmailService::sendCode($data['email'], fastapp_header('account-type', 1));
    }

    public function bindMobile(string|int $code, string|int $mobile, string|int $vcode): bool
    {
        $uid = get_uid();
        if ($mobile && Account::query()->where(['code' => $code, 'mobile' => $mobile])->where('gene_id', '!=', $uid)->exists()) throw new TipException(fastapp_t('account.email_bind'));
        if (FastappSmsService::validate($mobile, $vcode, $code)) {
            api_dispatch(new AccountBindMobileEvent($uid, $code, $mobile));
            return true;
        }
        return false;
    }

    public function bindEmail(string $email, string|int $vcode): bool
    {
        $uid = get_uid();
        if ($email && Account::query()->where(['email' => $email])->where('gene_id', '!=', $uid)->exists()) throw new TipException(fastapp_t('account.email_bind'));
        FastappEmailService::validate($email, $vcode);
        api_dispatch(new AccountBindEmailEvent($uid, $email));
        return true;
    }

    public function sendBindMobileVcode(): bool
    {
        $md = Account::query()->where(['gene_id' => get_uid()])->firstOrFail();
        if (!$md->mobile) throw new TipException(fastapp_t('account.unbound_mobile'));
        return FastappSmsService::sendCode($md->mobile, $md->code, fastapp_header('account-type', 1));
    }

    public function sendBindEmailVcode(): bool
    {
        $md = Account::query()->where(['gene_id' => get_uid()])->firstOrFail();
        if (!$md->email) throw new TipException(fastapp_t('account.unbound_email'));
        return FastappEmailService::sendCode($md->email, fastapp_header('account-type', 1));
    }

    public function changeMobile(array $data): bool
    {
        $ac = Account::query()->where(['gene_id' => get_uid()])->firstOrFail();
        if ($ac->mobile != $data['mobile']) throw new TipException(fastapp_t('account.original_mobile_error'));
        if (Account::query()->where(['mobile' => $data['newmobile'], 'type' => $ac->type])->exists()) throw new TipException(fastapp_t('account.mobile_used'));
        FastappSmsService::validate($data['newmobile'], $data['vcode'], $data['code']);
        if ($ac->update(['code' => $data['code'], 'mobile' => $data['newmobile']])) return true;
        throw new TipException(fastapp_t('fastapp.modification_failed'));
    }

    public function changeEmail(array $data): bool
    {
        $ac = Account::query()->where(['gene_id' => get_uid()])->firstOrFail();
        if (Account::query()->where(['email' => $data['email'], 'type' => $ac->type])->exists()) throw new TipException(fastapp_t('account.email_used'));
        FastappEmailService::validate($data['email'], $data['vcode']);
        if ($ac->update(['email' => $data['email']])) return true;
        throw new TipException(fastapp_t('fastapp.modification_failed'));
    }

    public function setTransactionPassword(string $password): bool
    {
        return AccountSettingService::setTransactionPassword($password, get_uid());
    }

    public function resetTransactionPassword(array $param): bool
    {
        $uid = get_uid();
        if ($param['type'] == 1) SecurityCenterService::validateGoogle2FA($param['vcode']);
        if ($param['type'] == 2) {
            $email = Account::query()->where(['gene_id' => $uid])->value('email');
            FastappEmailService::validate($email, $param['vcode']);
        }
        if ($param['type'] == 3) {
            $ac = Account::query()->where(['gene_id' => $uid])->first(['mobile', 'code']);
            FastappSmsService::validate($ac->mobile, $param['vcode'], $ac->code);
        }
        if ($param['type'] == 4) {
            $val = AccountSetting::query()->where(['user_id' => $uid, 'key' => 'transaction_password'])->value('val');
            if ($val != $param['vcode']) throw new TipException(fastapp_t('account.original_transaction_password_error'));
        }
        return AccountSettingService::setTransactionPassword($param['password'], get_uid(), true);
    }

    public static function checkTransactionPassword(string $password, int $uid = 0): array
    {
        $uid = $uid ?: get_uid();
        $config = AccountSettingService::config($uid);
        if (empty($config['transaction_password'])) throw new TipException(fastapp_t('account.transaction_password_not_set'));
        if ($config['transaction_password'] == $password) return ['status' => 1];
        return ['status' => 0];
    }

    public function loginByTelegram(array $data): array
    {
        $account_type = fastapp_header('account-type', 1);
        $app_tg_bot_token = api_cache()->getAppSetting($account_type)['app_tg_bot_token'];
        if (empty($data['login_type']) || $data['login_type'] == 1) {
            if (!Telegram::checkTelegramAuthorization($data, $app_tg_bot_token)) throw new TipException(fastapp_t('account.login_failed'));
        } else {
            $result = Telegram::verifyTelegramHash($data['hash'], $app_tg_bot_token);
            if (!$result) throw new TipException(fastapp_t('account.login_failed'));
            $data = array_merge($data, $result['user']);
        }
        $md = AccountOtherRelated::query()->firstOrCreate([
            'unique_code' => $data['id'],
            'related_type' => AccountOtherRelated::TYPE_TELEGRAM,
            'account_type' => $account_type
        ]);
        if ($md && $md->account) {
            if ($md->account->status == 2) throw new TipException(fastapp_t('account.account_cancelled'));
            if (!empty($data['push_client'])) $this->setPushClient($data['push_client'], $md->account->gene_id);
            set_user_cache(['tgid' => $data['id']], $md->account->gene_id);
            return $this->tokenFormat($md->account);
        }
        if (empty($data['direct'])) return [
            'to_bind' => 1,
            'related_type' => AccountOtherRelated::TYPE_TELEGRAM,
            'unique_code' => $md->unique_code
        ];

        $data['type'] = $account_type;
        if (isset($data['username'])) {
            $data['nickname'] = $data['username'];
            unset($data['username']);
        } else $data['nickname'] = $data['first_name'];
        $ac = $this->create($data);
        $md->update(['user_id' => $ac->gene_id]);
        set_user_cache(['tgid' => $data['id']], $ac->gene_id);
        return $this->tokenFormat($ac);
    }

    public function bindTelegram(array $data): array
    {
        FastappSmsService::validate($data['mobile'], $data['vcode'], $data['code']);
        $data['type'] = fastapp_header('account-type', 1);
        $md = Account::query()->where(['code' => $data['code'], 'mobile' => $data['mobile'], 'type' => $data['type']])->first();
        if ($data['related_type'] == AccountOtherRelated::TYPE_TELEGRAM && !$md) $md = $this->create($data);
        $map = [
            'unique_code' => $data['unique_code'],
            'related_type' => AccountOtherRelated::TYPE_TELEGRAM,
            'account_type' => $data['type']
        ];
        AccountOtherRelated::query()->where($map)->update(['user_id' => $md->gene_id]);
        return $this->tokenFormat($md);
    }

    public function checkVcode(array $param): array
    {
        try {
            if ($param['type'] == 1) FastappSmsService::validate($param['key'], $param['vcode'], $param['code'], false);
            else  FastappEmailService::validate($param['key'], $param['vcode'], false);
            return ['status' => 1];
        } catch (\Throwable) {
        }
        return ['status' => 0];
    }

    public function resetPassword(array $param): bool
    {
        $map['type'] = fastapp_header('account-type', 1);
        if ($param['type'] == 1) {
            $map['mobile'] = $param['key'];
            $ac = Account::query()->where($map)->firstOrFail();
            FastappSmsService::validate($param['key'], $param['vcode'], $param['code']);
        } else {
            $map['email'] = $param['key'];
            $ac = Account::query()->where($map)->firstOrFail();
            FastappEmailService::validate($param['key'], $param['vcode']);
        }
        return $ac->update(['password' => $param['password']]);
    }

    public function loginByAddress(string $symbol, string $address): array
    {
        if ($symbol == 'TRX' && !(new \TronApi\Address($address))->isValid()) throw new TipException(fastapp_t('fastapp.invalid_address'));
        if ($symbol == 'TON' && !\Olifanton\Interop\Address::isValid($address)) throw new TipException(fastapp_t('fastapp.invalid_address'));
        if (in_array($symbol, ['ETH', 'BNB']) && !\Web3\Validators\AddressValidator::validate($address)) throw new TipException(fastapp_t('fastapp.invalid_address'));
        $md = AccountOtherAddress::query()->firstOrCreate(['symbol' => $symbol, 'address' => $address]);
        if (!$md->user_id) {
            $ac = $this->create(['nickname' => $address, 'type' => fastapp_header('account-type', 1)]);
            $ac->update(['user_id' => $ac->gene_id]);
        } else {
            $ac = Account::query()->where(['gene_id' => $md->user_id])->first();
        }
        return $this->tokenFormat($ac);
    }
}